Skip to content

[api][runtime][python] Add minimal Event lineage#923

Open
rosemarYuan wants to merge 3 commits into
apache:mainfrom
rosemarYuan:feature/eventlineage
Open

[api][runtime][python] Add minimal Event lineage#923
rosemarYuan wants to merge 3 commits into
apache:mainfrom
rosemarYuan:feature/eventlineage

Conversation

@rosemarYuan

@rosemarYuan rosemarYuan commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Linked issue: Closes #841

Related discussions:

Purpose of change

The Event Log currently records individual Events but does not preserve enough information to reconstruct the direct causal path between an Event, the Action it triggered, and the Events emitted by that Action.

This PR implements the minimal Event-lineage phase discussed in #710.

It adds two framework-managed fields to each Event emitted by an Action:

  • upstreamEventId: the ID of the Event consumed by the emitting Action;
  • upstreamActionName: the name of the Action that emitted the current Event.

Together with the existing per-occurrence Event ID, these fields are sufficient to reconstruct an InputEvent-rooted Event-Action-Event causal tree from the available Event Log records.

This PR deliberately does not implement the complete execution-tracing model proposed in #900. Run identity, concrete Action invocation identity, execution hierarchy, lifecycle, retry/replay semantics, ordering, and failure details remain part of that separate design.

A real Runtime execution was verified with the following causal path:

InputEvent(input=1)
  -> action1
  -> MiddleEvent(num=2)
  -> action2
  -> OutputEvent(output=4)

The File Event Log contained three physical Event records:

Event upstreamEventId upstreamActionName
_input_event absent absent
MiddleEvent InputEvent ID action1
_output_event MiddleEvent ID action2

Running tools/reconstruct_trace_tree.py against that Runtime log produced:

Trace Tree 1
  _input_event
    [Action: action1]
      MiddleEvent
        [Action: action2]
          _output_event

The JSON reconstruction contained one root, all three Event nodes, the two expected virtual Action edges, and no warnings.

Tests

Add e2e test to review Trace Tree python/flink_agents/e2e_tests/e2e_tests_integration/python_event_logging_test.py

API

This PR changes the default Python Event ID generation strategy from content-derived IDs to UUIDv4.

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

@github-actions github-actions Bot added doc-label-missing The Bot applies this label either because none or multiple labels were provided. fixVersion/0.4.0 priority/major Default priority of the PR or issue. doc-included Your PR already contains the necessary documentation updates. and removed doc-label-missing The Bot applies this label either because none or multiple labels were provided. labels Jul 23, 2026
@joeyutong

Copy link
Copy Markdown
Collaborator

One ActionStateStore replay semantic seems worth making explicit before merging. When a completed action result is reused, the runtime returns the stored output Events and rebinds their lineage to the current triggering Event while preserving the output Event IDs. The resulting behavior is:

first execution: E1 -> A -> O1
reused execution: E2 -> A(reused) -> O1

I think this is a reasonable semantic, but it is currently implicit. Could we document it and add a test that asserts O1.id is preserved while its upstreamEventId changes from E1 to E2? This also establishes that the same logical Event ID may appear in multiple Event Log records across reuse, which reconstruction needs to tolerate.

warnings: list[dict[str, str]] = []
nodes: dict[str, dict[str, Any]] = {}
for event_id, matching_records in records_by_id.items():
if len(matching_records) > 1:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActionStateStore reuse can legitimately log the same Event ID again, possibly with different upstream lineage. Treating every repeated ID as invalid and dropping all matching records loses valid replay branches. Could reconstruction model this as a DAG instead: deduplicate repeated (event ID, lineage edge) observations, retain distinct lineage edges for the same Event ID, and report an ID conflict only when the Event type or content is inconsistent?

}

/** Copies framework-managed metadata when reconstructing a typed Event. */
protected void copyFrameworkMetadataFrom(Event event) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typed reconstruction represents the same Event occurrence, but preserving that occurrence currently depends on every custom fromEvent implementation remembering both to pass the ID separately and to call this helper. Could the framework own a reconstruction template/factory so a custom subtype only maps its payload? Otherwise an omitted helper call silently loses sourceTimestamp and lineage.

if name != "id":
object.__setattr__(self, "id", self._generate_content_based_id())

def with_framework_metadata_from(self, source: "Event") -> Self:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Java and Python helpers currently have different contracts: Python returns a copy and includes the Event ID, while Java mutates the current object and does not copy the ID. Also, “framework metadata” describes ownership rather than the Event-domain meaning. Could the names and contracts be aligned around preserving/reconstructing the same Event occurrence?

}
}

private static void setOutputEventLineage(ActionTask actionTask, List<Event> outputEvents) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This rule describes how a concrete Action invocation finalizes its output Events, rather than Operator scheduling. Could it be owned by ActionTask, which already holds the triggering Event and Action? That would keep the Operator independent of lineage field details and provide one place to reject outputEvent.id == triggerEvent.id; currently ctx.sendEvent(event) can create a self-loop and mutate the triggering Event shared by sibling ActionTasks.

# Set fallback if not provided in kwargs
if "fallback" not in kwargs:
kwargs["fallback"] = self.__serialize_unknown
if "by_alias" not in kwargs:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Defaulting by_alias to True here changes serialization for every alias declared by a custom Event subclass, not only the two lineage fields. Could the camelCase mapping be scoped to the lineage fields, or should this broader compatibility change be documented and covered by a custom-alias test?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-included Your PR already contains the necessary documentation updates. fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Add lineage fields to the event log for per-run trace reconstruction

2 participants